在昨天終於把所有的Design Principle給講完了,今天開始進入Design Pattern。首先是創建型模型,其中單例模式為最常見的模式之一。讓我們開始了解什麼是單例模式。
Singleton = one instance ONLY
相信大家只要查過Singleton Pattern,就會出現這一句話。那到底one instance ONLY
是什麼意思呢?
我們用一個簡單的問題來了解!!
送貨員要從送貨進超市,或是要從超市取貨送給客戶,我們可以用以下的程式碼實作這個情境。
public class NotSinglePattern {
public static void main(String args[]) {
Supermarket mSupermarket1 = new Supermarket();
Supermarket mSupermarket2 = new Supermarket();
Freight freight1 = new Freight(mSupermarket1);
Freight freight2 = new Freight(mSupermarket2);
System.out.print("同一家超市?");
if(mSupermarket1.equals(mSupermarket2)){
System.out.println("yes");
}else {
System.out.println("no");
}
freight1.moveIn(30);
System.out.println("freight1搬完後商品數量:"+freight1.mSupermarket.getQuantity());
freight2.moveOut(50);
System.out.println("freight2搬完後商品數量:"+freight2.mSupermarket.getQuantity());
}
}
class Supermarket {
private int quantity = 100;
public Supermarket() {
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public int getQuantity() {
return quantity;
}
}
class Freight{
public Supermarket mSupermarket;
public Freight(Supermarket supermarket){
mSupermarket = supermarket;
}
public void moveIn(int i){
mSupermarket.setQuantity(mSupermarket.getQuantity()+i);
}
public void moveOut(int i){
mSupermarket.setQuantity(mSupermarket.getQuantity()-i);
}
}
output:
同一家超市?no
freight1搬完後商品數量:130
freight2搬完後商品數量:50
由上述程式碼的來看,一號送貨員要送進30件貨進超市,二號送貨員要從超市拿50件貨送給客戶,超商原本的貨物量有100件。所以一號送完貨物變130件,二號取完貨物應該變80件,但結果卻是50。而在結果第一行顯示兩個送貨員分別去到不同的市場,所以出現了問題。但我們希望他是到同一個超市送貨或取貨,所以我們修改一下程式:
public class SinglePattern {
public static void main(String args[]) {
Supermarket mSupermarket1 = Supermarket.getInstance();
Supermarket mSupermarket2 = Supermarket.getInstance();
Freight freight1 = new Freight(mSupermarket1);
Freight freight2 = new Freight(mSupermarket2);
System.out.print("同一家超市?");
if(mSupermarket1.equals(mSupermarket2)){
System.out.println("yes");
}else {
System.out.println("no");
}
freight1.moveIn(30);
System.out.println("freight1搬完後商品數量:"+freight1.mSupermarket.getQuantity());
freight2.moveOut(50);
System.out.println("freight2搬完後商品數量:"+freight2.mSupermarket.getQuantity());
}
}
class Supermarket {
private int quantity = 100;
private static Supermarket uniqueInstance = new Supermarket();
public static Supermarket getInstance() {
return uniqueInstance;
}
private Supermarket() {
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public int getQuantity() {
return quantity;
}
}
output: :
同一家超市?yes
freight1搬完後商品數量:130
freight2搬完後商品數量:80
在一開始就把超市的實體建立起來,如此一來就只會有一間超市,送貨員就可以在同一個超市送貨或取貨了!!
在程式開發上,常常也會遇到需要共用同一個物件的問題,如同第一篇提到,Design Pattern是為了解決重複出現的問題而衍生出來的解決方案。
而Singleton便是今天這個案例的解決方案!!而Singleton Pattern在不同的情境也會有不同的方法。
class Supermarket {
private int quantity = 100;
private static Supermarket uniqueInstance = new Supermarket();
private Supermarket() {
}
public static Supermarket getInstance() {
return uniqueInstance;
}
// ...略
}
將原本的constructor宣告成private,取而代之的是開放一個getInstance()的方法,供外部使用此類別。這樣的好處在於,如果要重複使用這個class,不會每次都有一個新的物件產生,原本的物件可以重複使用。
不過因為static的關係,在program啟動之後就會在memory裡面存了這個實體物件,但這個物件不一定時常被存取,不需要一開始就準備好這個實體物件,所以可以把程式改成lazy Initialization
。
class Supermarket {
private int quantity = 100;
private static Supermarket uniqueInstance = null;
private Supermarket() {
}
public static Supermarket getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Supermarket();
}
return uniqueInstance;
}
// ...略
}
Lazy Initialization
會在getInstance()時判斷uniqueInstance是否存在,如果不存在才建立,存在的話就直接回傳物件,如此一來就達到使用單一物件的目標了!!
但如果multi thread呼叫getInstance()會發生什麼事?我們把上述程式碼稍做修改:
public class SinglePattern {
public static void main(String args[]) {
Thread t1 = new FreightThread(1);
Thread t2 = new FreightThread(2);
t1.start();
t2.start();
}
}
class FreightThread extends Thread {
private String x;
public FreightThread(int x){
this.x = String.valueOf(x);
}
public void run(){
Supermarket mSupermarket = Supermarket.getInstance();
Freight freight = new Freight(mSupermarket);
freight.moveIn(30);
System.out.println( x + "號已送達!!" + "商品數量:" + freight.mSupermarket.getQuantity());
}
}
class Supermarket {
private int quantity = 100;
private static Supermarket uniqueInstance = null;
private Supermarket() {
}
public static Supermarket getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Supermarket();
System.out.println("建立 Supermarket");
}
return uniqueInstance;
}
// ...略
}
output:
建立 Supermarket
建立 Supermarket
1號已送達!!商品數量:130
2號已送達!!商品數量:130
結果我們發現,預期的數量應該是160,送貨員並沒有把貨送到同個地點,所以結果與預期不同。為什麼會這樣呢?
thread1執行new Singleton()時,可能thread2就也執行到了new Singleton(),就會建立兩個物件,這並不是我們想要看到的結果。
class Supermarket {
private int quantity = 100;
private static Supermarket uniqueInstance = null;
private Supermarket() {
}
public static synchronized Supermarket getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Supermarket();
System.out.println("建立 Supermarket");
}
return uniqueInstance;
}
// ... 略
}
output:
建立 Supermarket
2號已送達!!商品數量:130
1號已送達!!商品數量:160
於是乎我們將原本的Lazy Initialization
加上了synchronized
,在thread1使用getInstance時,getInstance會被lock住,這樣thread2就無法同時使用getInstance,必須等thread1結束,才可以使用,如此一來就不會產生兩個物件,也就確保超市只有一間,這樣送貨員也就可以將貨送至同一個超市。
可是這樣就會導致效能變差,而真正需要被lock的也只有new Singleton()這部分。那我們可以把synchronized往內移動,改良成效能更好的方案。
class Supermarket {
private int quantity = 100;
private static Supermarket uniqueInstance = null;
private Supermarket() {
}
public static Supermarket getInstance() {
if (uniqueInstance == null){
synchronized(Supermarket.class){
if(uniqueInstance == null) {
uniqueInstance = new Supermarket();
System.out.println("建立 Supermarket");
}
}
}
return uniqueInstance;
}
// ...略
}
output:
建立 Supermarket
2號已送達!!商品數量:130
1號已送達!!商品數量:160
Double-Checked Locking / DCL
只在uniqueInstance不存在時才使用synchronized,這樣不管在一般情況或是多執行緒下,重複使用同一個物件,也不會影響效能。不過,這種寫法相對複雜許多。
Static Inner Class
不但可以達到與Double-Checked Locking / DCL
相同的效果,而且寫法又更加的簡潔。
class Supermarket {
private int quantity = 100;
private static class LazyHolder {
private static Supermarket uniqueInstance = new Supermarket();
}
private Supermarket() {
}
public static Supermarket getInstance() {
return LazyHolder.uniqueInstance;
}
// ...略
}
output:
1號已送達!!商品數量:130
2號已送達!!商品數量:160
LazyHolder
在程式啟動時並不會載入內部類別,只有呼叫getInstance()時才會載入進行初始化。
Static Inner Class與double-checked locking / DCL的差別在哪呢?
在C++中都沒有問題,由於java內存模型的問題(無序寫入),會在使用過程中引入錯誤:JVM在啟動對象初始化操作後,就返回了,加入此時有其他線程來請求,instance已經不為null,而初始化還未完成,如果使用,會帶來一些錯誤。
上述我們介紹了五種寫法,從最一開始建立物件的Greed Singleton
、自行初始化的Lazy Initialization
、為了對付thread而衍生的Lazy Initialization + synchronized
、解決synchronized效能問題的Double-checked locking / DCL
,以及更加精簡的Static Inner Class
,這樣的順序也方便大家去記憶這五種方法,
不過下面還有一種方法,被譽為最為貼近Singleton Pattern的解決方案!!
enum
的寫法類似於Greed Singleton
,但整體來說更簡潔便利。
Effective Java作者Josh Bloch認爲enum
本身的特性不僅解決多執行緒同步問題,支援序列化機制,防止反序列化重新建立新的物件,以及絕對不會建立多個物件,為實現Singleton Pattern
最佳的解法。
public class SinglePattern {
public static void main(String args[]) {
Supermarket mSupermarket1 = Supermarket.INSTANCE;
Supermarket mSupermarket2 = Supermarket.INSTANCE;
Freight freight1 = new Freight(mSupermarket1);
Freight freight2 = new Freight(mSupermarket2);
System.out.print("同一家超市?");
if(mSupermarket1.equals(mSupermarket2)){
System.out.println("yes");
}else {
System.out.println("no");
}
freight1.moveIn(30);
System.out.println("freight1搬完後商品數量:"+freight1.mSupermarket.getQuantity());
freight2.moveOut(50);
System.out.println("freight2搬完後商品數量:"+freight2.mSupermarket.getQuantity());
}
}
enum Supermarket{
INSTANCE;
private int quantity = 100;
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public int getQuantity() {
return quantity;
}
}
output:
同一家超市?yes
freight1搬完後商品數量:130
freight2搬完後商品數量:80
此篇我們介紹了Singleton Pattern的概念,這邊還做個總結一下!!
實現方式 | 優點 | 缺點 |
---|---|---|
Greed Singleton | 1. 初始化時建立,保證只有一個物件 2. Thread-safe 3. 佔用內存小 | 不可控制初始化時機 |
Enum Singleton | 1. 初始化時建立,保證只有一個物件 2. Thread-safe 3. 寫法簡單 | 不可控制初始化時機 |
Lazy Initialization (Thread-unsafe) | 1. 使用時才建立 2. 節省資源 | Thread-unsafe |
Lazy Initialization (Thread-safe) | 1. 使用時才建立 2. Thread-safe | 耗費過多資源(同步) |
Double-checked locking / DCL | 1. Thread-safe 2. 節省資源 | 邏輯相對複雜 |
Static Inner Class | 1. Thread-safe 2. 節省資源 3. 寫法簡單 |
範例1:問題1
範例2:問題2
範例3:Lazy Initialization
範例4:Double-Checked Locking / DCL
範例5:Static Inner Class
範例6:Enum Singleton
設計模式學習 - Singleton Pattern
單例模式| 菜鳥教程